﻿/* Copyright 2010 Igor Francé
 * 
 * This file is part of ECMADoc (http://ecmadoc.codeplex.com).
 * 
 * ECMADoc is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * ECMADoc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with ECMADoc. 
 * If not, see http://www.gnu.org/licenses/.
 */
namespace EcmaDoc.Gui.Controls
{
	using System;
	using System.Windows.Forms;
	using System.Runtime.InteropServices;
	using System.ComponentModel;
	using System.Security.Permissions;
	using System.Security;
	using System.Threading;

	public class FileFolderBrowseDialog : CommonDialog
	{
		private const int MaxPathLength = 260;

		private PInvoke.BrowseFolderCallbackProc callback;
		private string descriptionText;
		private Environment.SpecialFolder rootFolder;
		private string selectedPath;
		private bool selectedPathNeedsCheck;
		private bool showNewFolderButton;
		private bool showEditBox;
		private bool showBothFilesAndFolders;
		private bool newStyle = true;
		private bool showFullPathInEditBox = true;
		private bool dontIncludeNetworkFoldersBelowDomainLevel;
		private BrowseFlags uiFlags;
		private IntPtr hwndEdit;
		private IntPtr rootFolderLocation;

		// ctor
		public FileFolderBrowseDialog()
		{
			this.ResetDialog();
		}

		[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
		public new event EventHandler HelpRequest
		{
			add
			{
				base.HelpRequest += value;
			}
			remove
			{
				base.HelpRequest -= value;
			}
		}

		/// <summary>
		/// This description appears near the top of the dialog box, providing direction to the user.
		/// </summary>
		public string Description
		{
			get
			{
				return this.descriptionText;
			}
			set
			{
				this.descriptionText = (value == null) ? string.Empty : value;
			}
		}

		public Environment.SpecialFolder RootFolder
		{
			get
			{
				return this.rootFolder;
			}
			set
			{
				if (!Enum.IsDefined(typeof(Environment.SpecialFolder), value))
				{
					throw new InvalidEnumArgumentException("value", (int) value, typeof(Environment.SpecialFolder));
				}
				this.rootFolder = value;
			}
		}

		/// <summary>
		/// Set or get the selected path.  
		/// </summary>
		public string SelectedPath
		{
			get
			{
				if (((this.selectedPath != null) && (this.selectedPath.Length != 0)) && this.selectedPathNeedsCheck)
				{
					new FileIOPermission(FileIOPermissionAccess.PathDiscovery, this.selectedPath).Demand();
					this.selectedPathNeedsCheck = false;
				}
				return this.selectedPath;
			}
			set
			{
				this.selectedPath = (value == null) ? string.Empty : value;
				this.selectedPathNeedsCheck = true;
			}
		}

		/// <summary>
		/// Enable or disable the "New Folder" button in the browser dialog.
		/// </summary>
		public bool ShowNewFolderButton
		{
			get
			{
				return this.showNewFolderButton;
			}
			set
			{
				this.showNewFolderButton = value;
			}
		}

		/// <summary>
		/// Show an "edit box" in the folder browser.
		/// </summary>
		/// <remarks>
		/// The "edit box" normally shows the name of the selected folder.  
		/// The user may also type a pathname directly into the edit box.  
		/// </remarks>
		/// <seealso cref="ShowFullPathInEditBox"/>
		public bool ShowEditBox
		{
			get
			{
				return this.showEditBox;
			}
			set
			{
				this.showEditBox = value;
			}
		}

		/// <summary>
		/// Set whether to use the New Folder Browser dialog style.
		/// </summary>
		/// <remarks>
		/// The new style is resizable and includes a "New Folder" button.
		/// </remarks>
		public bool NewStyle
		{
			get
			{
				return this.newStyle;
			}
			set
			{
				this.newStyle = value;
			}
		}

		public bool DontIncludeNetworkFoldersBelowDomainLevel
		{
			get { return this.dontIncludeNetworkFoldersBelowDomainLevel; }
			set { this.dontIncludeNetworkFoldersBelowDomainLevel = value; }
		}

		/// <summary>
		/// Show the full path in the edit box as the user selects it. 
		/// </summary>
		/// <remarks>
		/// This works only if ShowEditBox is also set to true. 
		/// </remarks>
		public bool ShowFullPathInEditBox
		{
			get { return this.showFullPathInEditBox; }
			set { this.showFullPathInEditBox = value; }
		}

		public bool ShowBothFilesAndFolders
		{
			get { return this.showBothFilesAndFolders; }
			set { this.showBothFilesAndFolders = value; }
		}

		public static FileFolderBrowseDialog PrinterBrowser()
		{
			FileFolderBrowseDialog x = new FileFolderBrowseDialog();
			// avoid MBRO comppiler warning when passing _rootFolderLocation as a ref:
			x.BecomePrinterBrowser();
			return x;
		}

		public static FileFolderBrowseDialog ComputerBrowser()
		{
			FileFolderBrowseDialog x = new FileFolderBrowseDialog();
			// avoid MBRO comppiler warning when passing _rootFolderLocation as a ref:
			x.BecomeComputerBrowser();
			return x;
		}

		private void BecomePrinterBrowser()
		{
			uiFlags |= BrowseFlags.BIF_BROWSEFORPRINTER;
			Description = "Select a printer:";
			PInvoke.Shell32.SHGetSpecialFolderLocation(IntPtr.Zero, (int) CSIDL.PRINTERS, ref this.rootFolderLocation);
			ShowNewFolderButton = false;
			ShowEditBox = false;
		}

		private void BecomeComputerBrowser()
		{
			uiFlags |= BrowseFlags.BIF_BROWSEFORCOMPUTER;
			Description = "Select a computer:";
			PInvoke.Shell32.SHGetSpecialFolderLocation(IntPtr.Zero, (int) CSIDL.NETWORK, ref this.rootFolderLocation);
			ShowNewFolderButton = false;
			ShowEditBox = false;
		}

		private int FolderBrowserCallback(IntPtr hwnd, int msg, IntPtr lParam, IntPtr lpData)
		{
			BrowseForFolderMessages message = (BrowseForFolderMessages) msg;
			switch (message)
			{
				case BrowseForFolderMessages.BFFM_INITIALIZED:
					if (this.selectedPath.Length != 0)
					{
						PInvoke.User32.SendMessage(new HandleRef(null, hwnd), (int) BrowseForFolderMessages.BFFM_SETSELECTIONW, 1, this.selectedPath);
						if (this.showEditBox && this.showFullPathInEditBox)
						{
							// get handle to the Edit box inside the Folder Browser Dialog
							hwndEdit = PInvoke.User32.FindWindowEx(new HandleRef(null, hwnd), IntPtr.Zero, "Edit", null);
							PInvoke.User32.SetWindowText(hwndEdit, this.selectedPath);
						}
					}
					break;

				case BrowseForFolderMessages.BFFM_SELCHANGED:
					IntPtr pidl = lParam;
					if (pidl != IntPtr.Zero)
					{
						if (((uiFlags & BrowseFlags.BIF_BROWSEFORPRINTER) == BrowseFlags.BIF_BROWSEFORPRINTER) ||
							((uiFlags & BrowseFlags.BIF_BROWSEFORCOMPUTER) == BrowseFlags.BIF_BROWSEFORCOMPUTER))
						{
							// we're browsing for a printer or computer, enable the OK button unconditionally.
							PInvoke.User32.SendMessage(new HandleRef(null, hwnd), (int) BrowseForFolderMessages.BFFM_ENABLEOK, 0, 1);
						}
						else
						{
							IntPtr pszPath = Marshal.AllocHGlobal(MaxPathLength * Marshal.SystemDefaultCharSize);
							bool haveValidPath = PInvoke.Shell32.SHGetPathFromIDList(pidl, pszPath);
							String displayedPath = Marshal.PtrToStringAuto(pszPath);
							Marshal.FreeHGlobal(pszPath);
							// whether to enable the OK button or not. (if file is valid)
							PInvoke.User32.SendMessage(new HandleRef(null, hwnd), (int) BrowseForFolderMessages.BFFM_ENABLEOK, 0, haveValidPath ? 1 : 0);

							// Maybe set the Edit Box text to the Full Folder path
							if (haveValidPath && !String.IsNullOrEmpty(displayedPath))
							{
								if (showEditBox && showFullPathInEditBox)
								{
									if (hwndEdit != IntPtr.Zero)
										PInvoke.User32.SetWindowText(hwndEdit, displayedPath);
								}

								if ((uiFlags & BrowseFlags.BIF_STATUSTEXT) == BrowseFlags.BIF_STATUSTEXT)
									PInvoke.User32.SendMessage(new HandleRef(null, hwnd), (int) BrowseForFolderMessages.BFFM_SETSTATUSTEXT, 0, displayedPath);
							}
						}
					}
					break;
			}
			return 0;
		}

		private static PInvoke.IMalloc GetSHMalloc()
		{
			PInvoke.IMalloc[] ppMalloc = new PInvoke.IMalloc[1];
			PInvoke.Shell32.SHGetMalloc(ppMalloc);
			return ppMalloc[0];
		}

		private void ResetDialog()
		{
			this.rootFolder = (Environment.SpecialFolder) 0;
			this.descriptionText = string.Empty;
			this.selectedPath = string.Empty;
			this.selectedPathNeedsCheck = false;
			this.showNewFolderButton = true;
			this.showEditBox = true;
			this.newStyle = true;
			this.dontIncludeNetworkFoldersBelowDomainLevel = false;
			this.hwndEdit = IntPtr.Zero;
			this.rootFolderLocation = IntPtr.Zero;
		}

		public override void Reset()
		{
			ResetDialog();
		}

		protected override bool RunDialog(IntPtr hWndOwner)
		{
			bool result = false;
			if (rootFolderLocation == IntPtr.Zero)
			{
				PInvoke.Shell32.SHGetSpecialFolderLocation(hWndOwner, (int) this.rootFolder, ref rootFolderLocation);
				if (rootFolderLocation == IntPtr.Zero)
				{
					PInvoke.Shell32.SHGetSpecialFolderLocation(hWndOwner, 0, ref rootFolderLocation);
					if (rootFolderLocation == IntPtr.Zero)
					{
						throw new InvalidOperationException("FolderBrowserDialogNoRootFolder");
					}
				}
			}
			hwndEdit = IntPtr.Zero;
			//_uiFlags = 0;
			if (dontIncludeNetworkFoldersBelowDomainLevel)
				uiFlags |= BrowseFlags.BIF_DONTGOBELOWDOMAIN;
			if (this.newStyle)
				uiFlags |= BrowseFlags.BIF_NEWDIALOGSTYLE;
			if (!this.showNewFolderButton)
				uiFlags |= BrowseFlags.BIF_NONEWFOLDERBUTTON;
			if (this.showEditBox)
				uiFlags |= BrowseFlags.BIF_EDITBOX;
			if (this.showBothFilesAndFolders)
				uiFlags |= BrowseFlags.BIF_BROWSEINCLUDEFILES;


			if (Control.CheckForIllegalCrossThreadCalls && (Application.OleRequired() != ApartmentState.STA))
			{
				throw new ThreadStateException("DebuggingException: ThreadMustBeSTA");
			}
			IntPtr pidl = IntPtr.Zero;
			IntPtr hglobal = IntPtr.Zero;
			IntPtr pszPath = IntPtr.Zero;
			try
			{
				PInvoke.BROWSEINFO browseInfo = new PInvoke.BROWSEINFO();
				hglobal = Marshal.AllocHGlobal(MaxPathLength * Marshal.SystemDefaultCharSize);
				pszPath = Marshal.AllocHGlobal(MaxPathLength * Marshal.SystemDefaultCharSize);
				this.callback = new PInvoke.BrowseFolderCallbackProc(this.FolderBrowserCallback);
				browseInfo.pidlRoot = rootFolderLocation;
				browseInfo.Owner = hWndOwner;
				browseInfo.pszDisplayName = hglobal;
				browseInfo.Title = this.descriptionText;
				browseInfo.Flags = (int) uiFlags;
				browseInfo.callback = this.callback;
				browseInfo.lParam = IntPtr.Zero;
				browseInfo.iImage = 0;
				pidl = PInvoke.Shell32.SHBrowseForFolder(browseInfo);
				if (((uiFlags & BrowseFlags.BIF_BROWSEFORPRINTER) == BrowseFlags.BIF_BROWSEFORPRINTER) ||
				((uiFlags & BrowseFlags.BIF_BROWSEFORCOMPUTER) == BrowseFlags.BIF_BROWSEFORCOMPUTER))
				{
					this.selectedPath = Marshal.PtrToStringAuto(browseInfo.pszDisplayName);
					result = true;
				}
				else
				{
					if (pidl != IntPtr.Zero)
					{
						PInvoke.Shell32.SHGetPathFromIDList(pidl, pszPath);
						this.selectedPathNeedsCheck = true;
						this.selectedPath = Marshal.PtrToStringAuto(pszPath);
						result = true;
					}
				}
			}
			finally
			{
				PInvoke.IMalloc sHMalloc = GetSHMalloc();
				sHMalloc.Free(rootFolderLocation);
				rootFolderLocation = IntPtr.Zero;
				if (pidl != IntPtr.Zero)
				{
					sHMalloc.Free(pidl);
				}
				if (pszPath != IntPtr.Zero)
				{
					Marshal.FreeHGlobal(pszPath);
				}
				if (hglobal != IntPtr.Zero)
				{
					Marshal.FreeHGlobal(hglobal);
				}
				this.callback = null;
			}
			return result;
		}

		enum CSIDL
		{
			PRINTERS = 4,
			NETWORK = 0x12,
		}

		[Flags]
		enum BrowseFlags
		{
			BIF_DEFAULT = 0x0000,
			BIF_BROWSEFORCOMPUTER = 0x1000,
			BIF_BROWSEFORPRINTER = 0x2000,
			BIF_BROWSEINCLUDEFILES = 0x4000,
			BIF_BROWSEINCLUDEURLS = 0x0080,
			BIF_DONTGOBELOWDOMAIN = 0x0002,
			BIF_EDITBOX = 0x0010,
			BIF_NEWDIALOGSTYLE = 0x0040,
			BIF_NONEWFOLDERBUTTON = 0x0200,
			BIF_RETURNFSANCESTORS = 0x0008,
			BIF_RETURNONLYFSDIRS = 0x0001,
			BIF_SHAREABLE = 0x8000,
			BIF_STATUSTEXT = 0x0004,
			BIF_UAHINT = 0x0100,
			BIF_VALIDATE = 0x0020,
			BIF_NOTRANSLATETARGETS = 0x0400,
		}

		enum BrowseForFolderMessages
		{
			BFFM_INITIALIZED = 1,
			BFFM_SELCHANGED = 2,
			BFFM_VALIDATEFAILEDA = 3,
			BFFM_VALIDATEFAILEDW = 4,
			BFFM_IUNKNOWN = 5,

			BFFM_SETSTATUSTEXT = 0x464,
			BFFM_ENABLEOK = 0x465,
			BFFM_SETSELECTIONA = 0x466,
			BFFM_SETSELECTIONW = 0x467,
		}
	}

	internal static class PInvoke
	{
		static PInvoke() { }

		public delegate int BrowseFolderCallbackProc(IntPtr hwnd, int msg, IntPtr lParam, IntPtr lpData);

		internal static class User32
		{
			[DllImport("user32.dll", CharSet = CharSet.Auto)]
			public static extern IntPtr SendMessage(HandleRef hWnd, int msg, int wParam, string lParam);

			[DllImport("user32.dll", CharSet = CharSet.Auto)]
			public static extern IntPtr SendMessage(HandleRef hWnd, int msg, int wParam, int lParam);

			[DllImport("user32.dll", SetLastError = true)]
			//public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
			//public static extern IntPtr FindWindowEx(HandleRef hwndParent, HandleRef hwndChildAfter, string lpszClass, string lpszWindow);
			public static extern IntPtr FindWindowEx(HandleRef hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

			[DllImport("user32.dll", SetLastError = true)]
			public static extern Boolean SetWindowText(IntPtr hWnd, String text);
		}

		[ComImport]
		[Guid("00000002-0000-0000-c000-000000000046")]
		[SuppressUnmanagedCodeSecurity]
		[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
		public interface IMalloc
		{
			[PreserveSig]
			IntPtr Alloc(int cb);
			[PreserveSig]
			IntPtr Realloc(IntPtr pv, int cb);
			[PreserveSig]
			void Free(IntPtr pv);
			[PreserveSig]
			int GetSize(IntPtr pv);
			[PreserveSig]
			int DidAlloc(IntPtr pv);
			[PreserveSig]
			void HeapMinimize();
		}

		[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
		public class BROWSEINFO
		{
			public IntPtr Owner;
			public IntPtr pidlRoot;
			public IntPtr pszDisplayName;
			public string Title;
			public int Flags;
			public BrowseFolderCallbackProc callback;
			public IntPtr lParam;
			public int iImage;
		}

		[SuppressUnmanagedCodeSecurity]
		internal static class Shell32
		{
			// Methods
			[DllImport("shell32.dll", CharSet = CharSet.Auto)]
			public static extern IntPtr SHBrowseForFolder([In] PInvoke.BROWSEINFO lpbi);
			[DllImport("shell32.dll")]
			public static extern int SHGetMalloc([Out, MarshalAs(UnmanagedType.LPArray)] PInvoke.IMalloc[] ppMalloc);
			[DllImport("shell32.dll", CharSet = CharSet.Auto)]
			public static extern bool SHGetPathFromIDList(IntPtr pidl, IntPtr pszPath);
			[DllImport("shell32.dll")]
			public static extern int SHGetSpecialFolderLocation(IntPtr hwnd, int csidl, ref IntPtr ppidl);
		}
	}
}
